Skip to content

feat: implement cronjob primitive#19

Merged
sourcehawk merged 39 commits intomainfrom
feature/cronjob-primitive
Mar 25, 2026
Merged

feat: implement cronjob primitive#19
sourcehawk merged 39 commits intomainfrom
feature/cronjob-primitive

Conversation

@sourcehawk
Copy link
Owner

Implements the cronjob Kubernetes resource primitive following the pattern established by the existing ConfigMap and Deployment primitives.

Summary

  • Adds cronjob primitive package under pkg/primitives/cronjob/
  • Implements required lifecycle interfaces
  • Includes editors, mutator, flavors, and builder

Checklist

  • Compiles cleanly
  • Tests pass
  • Follows naming conventions in CONTEXT.md
  • Does not modify shared files

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new cronjob primitive to the Operator Component Framework, extending the primitives layer with first-class support for reconciling Kubernetes batch/v1 CronJobs (builder/resource, mutator + editors, flavors), plus accompanying docs and an example.

Changes:

  • Introduces pkg/primitives/cronjob with builder/resource lifecycle integration, mutation planning/apply, flavors, and default handlers.
  • Adds new mutation editors for CronJobSpec and JobSpec under pkg/mutation/editors.
  • Adds docs and an end-to-end example; also updates repo lint config and a few existing test files.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
pkg/primitives/cronjob/resource.go CronJob primitive resource wrapper around generic IntegrationResource
pkg/primitives/cronjob/mutator.go Plan/apply mutator for CronJob + containers/initContainers editing
pkg/primitives/cronjob/handlers.go Default operational + suspension handlers for CronJob
pkg/primitives/cronjob/flavors.go Field-application flavors for labels/annotations + pod template metadata
pkg/primitives/cronjob/builder.go Fluent builder wiring defaults, mutations, flavors, handlers, extractors
pkg/primitives/cronjob/mutator_test.go Extensive mutator behavior tests (ordering, selectors, nil-safety, etc.)
pkg/primitives/cronjob/handlers_test.go Tests for default operational/suspension handlers
pkg/primitives/cronjob/flavors_test.go Tests for flavor behavior and ordering
pkg/primitives/cronjob/builder_test.go Builder validation and option wiring tests
pkg/mutation/editors/cronjobspec.go New typed editor for batchv1.CronJobSpec
pkg/mutation/editors/cronjobspec_test.go Tests for CronJobSpecEditor methods
pkg/mutation/editors/jobspec.go New typed editor for batchv1.JobSpec
pkg/mutation/editors/jobspec_test.go Tests for JobSpecEditor methods
pkg/component/suite_test.go Removes revive nolint from dot-imports
pkg/component/component_test.go Removes revive nolint from dot-imports
internal/generic/resource_task_test.go Removes dupl nolint marker
internal/generic/resource_integration_test.go Removes dupl nolint marker
examples/cronjob-primitive/resources/cronjob.go Example resource factory assembling CronJob primitive with features/flavors
examples/cronjob-primitive/main.go Runnable fake-client reconciliation demo exercising feature/suspend flows
examples/cronjob-primitive/features/mutations.go Example feature mutations using the cronjob mutator/editors
examples/cronjob-primitive/app/controller.go Example controller wiring a CronJob-backed component
examples/cronjob-primitive/README.md Documentation for running and understanding the example
docs/primitives/cronjob.md New primitive documentation for CronJob
.golangci.yml Updates golangci-lint v2 config layout, enabled linters, and exclusions

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 3 comments.

@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:

  • docs/primitives/cronjob.md line 9: Fixed capabilities table to use OperationPending instead of Pending to match the actual status value from concepts.OperationalStatusPending
  • pkg/primitives/cronjob/resource.go line 27: Added resource_test.go with comprehensive resource-level tests covering Identity, Object, Mutate (baseline, with mutation, feature ordering, custom field applicator + error), ConvergingStatus (custom handler, default pending, default operational), DeleteOnSuspend (custom handler, default false), Suspend (default handler, custom handler), SuspensionStatus (custom handler, default suspending, default suspended), ExtractData, and ExtractData error

Intentionally ignored:

  • .golangci.yml line 10: Comment asks to update PR description/checklist or move shared file changes to a separate PR. This is a PR description concern, not a code change. The .golangci.yml change (upgrading to valid v2 config) was a necessary prerequisite for the cronjob primitive to build correctly and is already in a prior commit.

<!-- claude-review-cycle -->

Copilot AI review requested due to automatic review settings March 22, 2026 19:48
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings March 23, 2026 00:42
sourcehawk and others added 11 commits March 23, 2026 00:43
JobSpecEditor provides typed methods for Job spec fields (completions,
parallelism, backoff limit, etc.). CronJobSpecEditor provides typed
methods for CronJob spec fields (schedule, concurrency policy, time
zone, etc.). The suspend field is intentionally excluded from
CronJobSpecEditor as it is managed by the framework's suspension system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the CronJob primitive using IntegrationBuilder with
Operational and Suspendable concepts. Key behaviors:
- Operational status based on LastScheduleTime (Pending vs Operational)
- Suspension via spec.suspend=true (DeleteOnSuspend=false)
- Full pod-template mutation pipeline: metadata, cronjob spec, job spec,
  pod template metadata, pod spec, containers, init containers
- Flavors for preserving labels, annotations, and pod template metadata

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents the Integration lifecycle, mutation pipeline ordering,
editors (CronJobSpec, JobSpec, PodSpec, Container, ObjectMeta),
operational status semantics, and suspension behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Demonstrates building a CronJob resource with version-gated mutations,
tracing/metrics features, field preservation flavors, suspension via
spec.suspend, and data extraction. Uses the shared ExampleApp CRD and
a fake client for standalone execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix container field paths in docs mutation ordering table to use full
  CronJob path (spec.jobTemplate.spec.template.spec.containers)
- Use correct OperationPending status name in docs to match framework constant
- Explicitly convert LastScheduleTime to UTC before formatting timestamp
- Fix range variable address-of footgun in mutator_test.go findEnv helper

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix capabilities table in cronjob.md to use `OperationPending` instead
of `Pending` to match the actual status value from the concepts package.

Add resource_test.go for the cronjob primitive covering Identity, Object,
Mutate, feature ordering, custom field applicator, ConvergingStatus,
DeleteOnSuspend, Suspend, SuspensionStatus, and ExtractData.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@sourcehawk sourcehawk force-pushed the feature/cronjob-primitive branch from 563d078 to 5058194 Compare March 23, 2026 00:43
@sourcehawk
Copy link
Owner Author

approved

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

sourcehawk and others added 2 commits March 23, 2026 02:54
The FeatureMutator interface in internal/generic used an unexported
beginFeature() method, which meant primitive mutators in external
packages (cronjob, deployment, configmap) could never satisfy the
interface. The type assertion in ApplyMutations always returned false,
so feature boundaries were silently skipped for all primitives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:

  • beginFeature() unexported method bug (mutator.go:64): Exported beginFeature()BeginFeature() on the FeatureMutator interface and all three primitive mutators (cronjob, deployment, configmap). The unexported method meant the interface could never be satisfied by types outside internal/generic, so ApplyMutations silently skipped all feature boundaries. Updated all call sites and tests.

Intentionally ignored:

  • Doc || table delimiters (cronjob.md:13): Invalid — the tables in the file already use standard single-pipe | format. No || delimiters exist.
  • .golangci.yml shared file / PR description checklist (.golangci.yml:21): Valid observation about PR description accuracy, but not a code change. The .golangci.yml timeout change was a necessary build fix. Shared test file changes were required for the BeginFeature export.

@sourcehawk sourcehawk requested review from Copilot and removed request for Copilot March 25, 2026 16:05
@sourcehawk sourcehawk requested review from Copilot and removed request for Copilot March 25, 2026 16:11
@sourcehawk sourcehawk requested review from Copilot and removed request for Copilot March 25, 2026 16:12
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 7 comments.

Comment on lines +7 to 8
default: none
enable:
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.
- unused
- whitespace

settings:
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.
- name: unexported-return
- name: indent-error-flow
- name: errorf
exclusions:
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.
- name: unexported-return
- name: indent-error-flow
- name: errorf
exclusions:
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.

| Capability | Detail |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| **Operational tracking** | Reports `OperationPending` (never scheduled) or `Operational` (has scheduled at least once) |
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs reference OperationPending, but the framework concepts in code use OperationalStatusPending (and the tests assert concepts.OperationalStatusPending). Please align the documentation to the actual status names returned by the primitive to avoid confusing API consumers.

Suggested change
| **Operational tracking** | Reports `OperationPending` (never scheduled) or `Operational` (has scheduled at least once) |
| **Operational tracking** | Reports `OperationalStatusPending` (never scheduled) or `Operational` (has scheduled at least once) |

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +215
| Status | Condition |
| ------------------ | -------------------------------- |
| `OperationPending` | `Status.LastScheduleTime == nil` |
| `Operational` | `Status.LastScheduleTime != nil` |
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs reference OperationPending, but the framework concepts in code use OperationalStatusPending (and the tests assert concepts.OperationalStatusPending). Please align the documentation to the actual status names returned by the primitive to avoid confusing API consumers.

Copilot uses AI. Check for mistakes.
@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:
None

Intentionally ignored:

  • .golangci.yml (5 comments): Copilot flagged linters.default, linters.settings, linters.exclusions, and formatters.exclusions as invalid schema keys. However, this config uses version: "2" (golangci-lint v2 format), and these keys are correct for v2. Verified with golangci-lint config verify (v2.11.2) which passes with no errors.
  • docs/primitives/cronjob.md lines 11 and 215 (2 comments): Copilot suggested changing OperationPending to OperationalStatusPending. The docs correctly use the runtime string value "OperationPending" (defined at concepts.OperationalStatusPending OperationalStatus = "OperationPending"). OperationalStatusPending is the Go constant name, not the status string. The docs are accurate as-is.

<!-- claude-review-cycle -->

@sourcehawk sourcehawk requested review from Copilot and removed request for Copilot March 25, 2026 16:16
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.

Comment on lines +68 to +73
func (m *Mutator) EditObjectMetadata(edit func(*editors.ObjectMetaEditor) error) {
if edit == nil {
return
}
m.active.cronjobMetadataEdits = append(m.active.cronjobMetadataEdits, edit)
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the mutator registration methods append to m.active without guarding against m.active being nil. If any caller forgets to call BeginFeature() (or calls helper methods before BeginFeature), this will panic at runtime. Consider making the mutator safe by either (a) initializing a default plan in NewMutator, (b) lazily calling BeginFeature() the first time a registration method is called when m.active is nil, or (c) returning/recording an explicit error state (or at least a clear panic message) when BeginFeature hasn't been called.

Copilot uses AI. Check for mistakes.
…ations

Add requireActive() guard to all mutator registration methods so that
callers get an explicit panic message instead of a nil pointer dereference
if they forget to call BeginFeature() first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:

  • mutator.go L73: Added requireActive() guard to all mutator registration methods that access m.active. If BeginFeature() is not called first, callers now get a clear panic message (cronjob.Mutator: BeginFeature() must be called before registering mutations) instead of a nil pointer dereference. Added test coverage for this behavior.

Intentionally ignored:
None

<!-- claude-review-cycle -->

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants